TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import { notFound } from 'next/navigation';3import { Suspense } from 'react';4import { Tabs } from '@/components/ui/tabs';5import { Skeleton } from '@/components/ui/primitives';6import { AssetHeader } from '@/components/asset/asset-header';7import { ASSET_TABS, AnalysisTab, AuctionsTab, ComparablesTab, GradesTab, HistoryTab, ImagesTab, ListingsTab, OverviewTab, PopulationTab, SalesTab, SourcesTab, type AssetTab } from '@/components/asset/asset-tabs';8import { countAssetLots } from '@/lib/queries/market-lists';9import { getAssetBySlug, getAssetVariants, getAssetListings, getAssetLiveCounts, getLatestGuidePrice, getAssetImages, getPopulation, isWatchedBy } from '@/lib/queries/assets';10import { getCurrentUser } from '@/lib/auth/session';11import { catName, categoryPath } from '@/lib/taxonomy';12import { sp1, spEnum, spInt, type SP } from '@/lib/search-params';13import { fmtMoney } from '@/lib/format';1415export const revalidate = 120;1617const SITE = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.rareindex.io';1819export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {20 const { slug } = await params;21 const a = await getAssetBySlug(slug);22 if (!a) return { title: 'Asset not found' };23 const guide = a.rivUsd === null && a.latestSaleUsd === null ? await getLatestGuidePrice(a.id) : null;24 const price = a.rivUsd !== null ? ` · RIV ${fmtMoney(a.rivUsd)}` : a.latestSaleUsd !== null ? ` · last sale ${fmtMoney(a.latestSaleUsd)}` : guide ? ` · guide price ${fmtMoney(guide.priceUsd)} (${guide.sourceName})` : '';25 const description = `${a.title}${price}. ${catName(a.categorySlug)} price history, verified sales, live listings, rarity and liquidity on RareIndex.`;26 return {27 title: a.title,28 description,29 alternates: { canonical: `${SITE}/asset/${a.slug}` },30 openGraph: { title: a.title, description, url: `${SITE}/asset/${a.slug}`, images: [{ url: `/asset/${a.slug}/opengraph-image`, width: 1200, height: 630 }] },31 twitter: { card: 'summary_large_image', title: a.title, description },32 };33}3435export default async function AssetPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise<SP> }) {36 const { slug } = await params;37 const sp = await searchParams;38 const found = await getAssetBySlug(slug);39 if (!found) notFound();40 const tab = spEnum<AssetTab>(sp, 'tab', ASSET_TABS, 'overview');41 const [variants, live, imgs, user, popReports, lotCount] = await Promise.all([getAssetVariants(found.id), getAssetLiveCounts(found.id), getAssetImages(found.id, 12), getCurrentUser().catch(() => null), getPopulation(found.id), countAssetLots(found.id)]);42 // latest published population total across graders (for the rarity explainer); null when no report exists43 const population = popReports.length ? popReports.reduce((a, r) => (r.reportDate > a.reportDate ? r : a)).total : null;44 const watched = await isWatchedBy(user?.id ?? null, found.id);45 // asset_stats is rebuilt by the valuation worker; until then fall back to live counts from canonical tables.46 const asset = {47 ...found,48 salesCount: Math.max(found.salesCount, live.sales),49 sales30d: Math.max(found.sales30d, live.sales30d),50 activeListings: Math.max(found.activeListings, live.listings),51 sourcesCount: Math.max(found.sourcesCount, live.sources),52 latestSaleUsd: found.latestSaleUsd ?? live.latestSaleUsd,53 latestSaleAt: found.latestSaleAt ?? live.latestSaleAt,54 minAskUsd: found.minAskUsd ?? live.minAskUsd,55 };56 const vParam = sp1(sp, 'v');57 const variant = vParam ? variants.find((v) => v.id === vParam) ?? null : null;58 const page = spInt(sp, 'page');59 const href = (t: AssetTab, v: string | null = variant?.id ?? null) => `/asset/${asset.slug}?tab=${t}${v ? `&v=${v}` : ''}`;60 const [listingsForLd, guide] = await Promise.all([getAssetListings(asset.id, { limit: 20 }), (variant ? variant.rivUsd : asset.rivUsd) === null ? getLatestGuidePrice(asset.id, variant?.id ?? null) : Promise.resolve(null)]);61 const crumbs = categoryPath(asset.categorySlug);62 const seen = new Set<string>();63 const images = [asset.heroImageUrl ? { url: asset.heroImageUrl, caption: null } : null, ...imgs.map((im) => ({ url: im.url, caption: im.attribution ?? im.sourceId ?? null }))]64 .filter((im): im is { url: string; caption: string | null } => Boolean(im))65 .filter((im) => (seen.has(im.url) ? false : (seen.add(im.url), true)));6667 const jsonLd = [68 {69 '@context': 'https://schema.org',70 '@type': 'Product',71 name: asset.title,72 image: images.slice(0, 5).map((i) => i.url),73 description: asset.description ?? `${asset.title} — ${catName(asset.categorySlug)} collectible tracked by RareIndex.`,74 brand: asset.brand ? { '@type': 'Brand', name: asset.brand } : undefined,75 category: crumbs.map((c) => c.name).join(' > '),76 productID: asset.id,77 sku: asset.identifiers.sku ?? asset.identifiers.upc ?? undefined,78 url: `${SITE}/asset/${asset.slug}`,79 ...(listingsForLd.length && listingsForLd.some((l) => l.priceUsd !== null)80 ? {81 offers: {82 '@type': 'AggregateOffer',83 priceCurrency: 'USD',84 lowPrice: Math.min(...listingsForLd.filter((l) => l.priceUsd !== null).map((l) => l.priceUsd!)),85 highPrice: Math.max(...listingsForLd.filter((l) => l.priceUsd !== null).map((l) => l.priceUsd!)),86 offerCount: listingsForLd.length,87 offers: listingsForLd.slice(0, 5).map((l) => ({ '@type': 'Offer', price: l.priceUsd, priceCurrency: 'USD', url: l.sourceUrl, availability: 'https://schema.org/InStock', seller: l.seller ? { '@type': 'Organization', name: l.seller } : undefined })),88 },89 }90 : {}),91 },92 {93 '@context': 'https://schema.org',94 '@type': 'BreadcrumbList',95 itemListElement: [{ '@type': 'ListItem', position: 1, name: 'Markets', item: `${SITE}/markets` }, ...crumbs.map((c, i) => ({ '@type': 'ListItem', position: i + 2, name: c.name, item: `${SITE}/markets/${c.slug}` })), { '@type': 'ListItem', position: crumbs.length + 2, name: asset.title, item: `${SITE}/asset/${asset.slug}` }],96 },97 ];9899 return (100 <div className="pb-16 md:pb-0">101 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />102 <AssetHeader asset={asset} variants={variants} activeVariant={variant} tabHref={(v) => href(tab, v)} guide={guide} images={images} watched={watched} population={population} />103 <div className="sticky top-[var(--ri-header-h,56px)] z-30 -mx-4 bg-bg/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-bg/80 sm:static sm:mx-0 sm:bg-transparent sm:px-0 sm:backdrop-blur-none">104 <Tabs105 className="mt-6 mb-4"106 active={tab}107 tabs={[108 { id: 'overview', label: 'Overview', href: href('overview') },109 { id: 'sales', label: 'Sales', count: variant ? variant.salesCount : asset.salesCount, href: href('sales') },110 { id: 'listings', label: 'Listings', count: variant ? variant.activeListings : asset.activeListings, href: href('listings') },111 { id: 'auctions', label: 'Auctions', count: lotCount || null, href: href('auctions') },112 { id: 'grades', label: 'Grades', count: variants.filter((v) => v.grader && v.grader !== 'raw').length || null, href: href('grades') },113 { id: 'population', label: 'Population', href: href('population') },114 { id: 'images', label: 'Images', count: images.length || null, href: href('images') },115 { id: 'history', label: 'History', href: href('history') },116 { id: 'comparables', label: 'Comparables', href: href('comparables') },117 { id: 'analysis', label: 'AI Analysis', href: href('analysis') },118 { id: 'sources', label: 'Sources', count: asset.sourcesCount || null, href: href('sources') },119 ]}120 />121 </div>122 <Suspense key={`${tab}-${variant?.id ?? ''}-${page}`} fallback={<Skeleton className="h-96" />}>123 {tab === 'overview' ? <OverviewTab asset={asset} variant={variant} /> : null}124 {tab === 'sales' ? <SalesTab asset={asset} variant={variant} page={page} /> : null}125 {tab === 'listings' ? <ListingsTab asset={asset} variant={variant} /> : null}126 {tab === 'auctions' ? <AuctionsTab asset={asset} /> : null}127 {tab === 'grades' ? <GradesTab asset={asset} /> : null}128 {tab === 'population' ? <PopulationTab asset={asset} /> : null}129 {tab === 'images' ? <ImagesTab asset={asset} /> : null}130 {tab === 'history' ? <HistoryTab asset={asset} variant={variant} /> : null}131 {tab === 'comparables' ? <ComparablesTab asset={asset} /> : null}132 {tab === 'analysis' ? <AnalysisTab asset={asset} /> : null}133 {tab === 'sources' ? <SourcesTab asset={asset} /> : null}134 </Suspense>135 <p className="mt-6 text-[11px] leading-relaxed text-subtle">Valuations are estimates with a stated confidence; listing prices are not confirmed transactions; past performance does not guarantee future results. RareIndex does not authenticate items. Data attributed to its source; marks belong to their owners.</p>136 </div>137 );138}139